# #########################################
# ######### АДАПТИВНАЯ СЕТКА КРОВАТИ ############
# #########################################
# Автор: Frix_x#0161 #
# @версия: 3.0

# ЖУРНАЛ ИЗМЕНЕНИЙ:
#    v3.0: - добавлено использование тегов [exclude_object] для извлечения ограничивающей рамки первого слоя (большое спасибо Kyleisah за прекрасную идею и вдохновение)
#            макрос по-прежнему полностью совместим со старым способом с использованием параметра SIZE: он будет использовать его, если он указан, иначе
#            вернуться к методу [exclude_object] и, если оба недоступны, он сделает полную и обычную сетку кровати, как обычно.
#          - также добавлен параметр FORCE_MESH в сетку даже для очень мелких деталей
#          - удален RRI, который всегда добавлялся в вызове BED_MESH_CALIBRATE. Теперь он добавляется только тогда, когда он определен в разделе [bed_mesh]
#    v2.3: примечания по установке перемещены в соответствующий файл уценки в: doc > functions > Adaptive_bed_mesh.md
#    v2.2: удалено требование устанавливать mesh_pps в секции [bed_mesh]. Теперь это снова необязательно, как и должно быть.
#    v2.1: исправление для номинального меша (когда не используется параметр SIZE или SIZE=0_0_0_0)
#    v2.0: разделение на несколько макросов, чтобы можно было использовать центральную точку в положении зондирования z калибровочного стола перед созданием сетки
#    v1.1: исправлена ​​ошибка при разборе строки при использовании прописных букв в секции [bed_mesh]
#    v1.0: первый макрос адаптивной сетки кровати

# -------------------------------------------------- -------------------------------------------------- ----------------------
# Если вы хотите использовать его в своей конфигурации, установите его как отдельный макрос, как описано в
# установочный раздел этого файла: doc > features > Adaptive_bed_mesh.md
# -------------------------------------------------- -------------------------------------------------- ----------------------

# ## Что это? ###
# Адаптивная сетка кровати проста: это обычная сетка кровати, но только "где" и "когда" это необходимо.
# Иногда я печатаю мелкие детали, иногда я печатаю целые пластины, и мне нравится получать точную кровать_сетки (например, 9x9 или больше). Тем не менее, это займет
# много времени и бесполезно прощупывать всю пластину только на участке площадью 5 см². Вот тут-то и помогает адаптивная сетка кровати:
# 1. Получает координаты углов поверхности первого слоя либо из слайсера, либо из тегов [exclude_object]
# 2. Он вычисляет новый набор точек для зондирования в этой новой зоне, чтобы получить, по крайней мере, ту же точность, что и стандартная сетка пласта. Например, если
#     Обычная сетка кровати установлена ​​на 9x9 для 300 мм², тогда она будет вычислять 3x3 для поверхности 100 мм². Также, если по какой-либо причине ваши части находятся в
#     углу монтажной пластины (например, для поврежденного PEI в центре), он будет следовать за ними, чтобы исследовать именно эту область.
# 3. Поскольку вычисленные зондированные точки являются нечетными, он также вычислит новую относительную опорную индексную точку в центре зоны и сохранит
#     координаты этой точки, чтобы использовать их где-то еще (например, в измеренной точке плагина автоматической калибровки по оси z).
# 4. Чтобы пойти дальше, он не будет делать bed_mesh, если есть меньше чем 3x3 точек для исследования (только очень маленькая часть) и выбрать/изменить
#     алгоритм (бикубический/ларанжевой) в зависимости от размера и формы вычисляемой сетки (например, 3x3 против 3x9)

# Не стесняйтесь пинговать меня в Discord (Frix_x#0161), если вам нужна помощь или есть комментарии по улучшению :)


# ===========================================================================================================
# DO NOT MODIFY THOSE VARIABLES (they are used internaly by the adaptive bed mesh macro)
[gcode_macro _ADAPTIVE_MESH_VARIABLES]
variable_ready: False
variable_do_mesh: False
variable_do_nominal: False
variable_mesh_min: 0,0
variable_mesh_max: 0,0
variable_mesh_center: 0,0
variable_probe_count: 0,0
variable_rri: 0
variable_algo: "bicubic"
gcode:


[gcode_macro COMPUTE_MESH_PARAMETERS]
description: Compute the mesh parameters and store them for later use
gcode:
    # 1 ----- GET ORIGINAL BEDMESH PARAMS FROM CONFIG ----------------------
    {% set xMinConf, yMinConf = printer["configfile"].config["bed_mesh"]["mesh_min"].split(',')|map('trim')|map('int') %}
    {% set xMaxConf, yMaxConf = printer["configfile"].config["bed_mesh"]["mesh_max"].split(',')|map('trim')|map('int') %}
    {% set xProbeCntConf, yProbeCntConf = printer["configfile"].config["bed_mesh"]["probe_count"].split(',')|map('trim')|map('int') %}
    {% set algo = printer["configfile"].config["bed_mesh"]["algorithm"]|lower %}
    {% set xMeshPPS, yMeshPPS = (printer["configfile"].config["bed_mesh"]["mesh_pps"]|default('2,2')).split(',')|map('trim')|map('int') %}

    {% set margin = params.MARGIN|default(5)|int %} # additional margin to mesh around the first layer
    {% set force_mesh = params.FORCE_MESH|default(False) %} # force the mesh even if it's a small part (ie. computed less than 3x3)


    # 2 ----- GET FIRST LAYER COORDINATES and SIZE -------------------------------------
    # If the SIZE parameter is defined and not a dummy placeholder, we use it to do the adaptive bed mesh logic
    {% if params.SIZE is defined and params.SIZE != "0_0_0_0" %}
        RESPOND MSG="Got a SIZE parameter for the adaptive bed mesh"
        {% set xMinSpec, yMinSpec, xMaxSpec, yMaxSpec = params.SIZE.split('_')|map('trim')|map('int') %}
    
    {% elif printer.exclude_object.objects %}
        # Else if SIZE is not defined, we fallback to use the [exclude_object] tags
        # This method is derived from Kyleisah KAMP repository: https://github.com/kyleisah/Klipper-Adaptive-Meshing-Purging)
        RESPOND MSG="No SIZE parameter, using the [exclude_object] tags for the adaptive bed mesh"
        {% set eo_points = printer.exclude_object.objects|map(attribute='polygon')|sum(start=[]) %}
        {% set xMinSpec = eo_points|map(attribute=0)|min %}
        {% set yMinSpec = eo_points|map(attribute=1)|min %}
        {% set xMaxSpec = eo_points|map(attribute=0)|max %}
        {% set yMaxSpec = eo_points|map(attribute=1)|max %}
    
    {% else %}
        # Else if no SIZE parameter and no [exclude_object] tags, then we want to do a nominal bed mesh
        # so nothing to do here...
        RESPOND MSG="No info about the first layer coordinates, doing a nominal bed mesh instead of adaptive"
    {% endif %}


    # If the first layer size was correctly retrieved, we can do the adaptive bed mesh logic, else we
    # fallback to the original and nominal BED_MESH_CALIBRATE function (full bed probing)
    {% if xMinSpec and yMinSpec and xMaxSpec and yMaxSpec %}

        # 3 ----- APPLY MARGINS ----------------------------------------------
        # We use min/max function as we want it to be constrained by the original
        # bedmesh size. This will avoid going outside the machine limits
        {% set xMin = [xMinConf, (xMinSpec - margin)]|max %}
        {% set xMax = [xMaxConf, (xMaxSpec + margin)]|min %}
        {% set yMin = [yMinConf, (yMinSpec - margin)]|max %}
        {% set yMax = [yMaxConf, (yMaxSpec + margin)]|min %}

        # 4 ----- COMPUTE A NEW PROBE COUNT ----------------------------------
        # The goal is to have at least the same precision as from the config. So we compute an equivalent number
        # of probe points on each X/Y dimensions (distance between two points should be the same as in the config)
        {% set xProbeCnt = ((xMax - xMin) * xProbeCntConf / (xMaxConf - xMinConf))|round(0, 'ceil')|int %}
        {% set yProbeCnt = ((yMax - yMin) * yProbeCntConf / (yMaxConf - yMinConf))|round(0, 'ceil')|int %}

        # Then, three possibilities :
        # a) Both dimensions have less than 3 probe points : the bed_mesh is not needed as it's a small print (if not forced).
        # b) If one of the dimension is less than 3 and the other is greater. The print looks to be elongated and
        #    need the adaptive bed_mesh : we add probing points to the small direction to reach 3 and be able to do it.
        # c) If both direction are greater than 3, we need the adaptive bed_mesh and it's ok.
        # At the end we control (according to Klipper bed_mesh method: "_verify_algorithm") that the computed probe_count is
        # valid according to the choosen algorithm or change it if needed.
        {% if xProbeCnt < 3 and yProbeCnt < 3 %}
            {% if force_mesh %}
                RESPOND MSG="Bed mesh forced (small part detected): meshing 3x3..."
                {% set xProbeCnt = 3 %}
                {% set yProbeCnt = 3 %}
                {% set rRefIndex = 4 %}
                {% set algo = "lagrange" %}
                {% set xCenter = xMin + ((xMax - xMin) / 2) %}
                {% set yCenter = yMin + ((yMax - yMin) / 2) %}
                {% set mesh_min = "%d,%d"|format(xMin, yMin) %}
                {% set mesh_max = "%d,%d"|format(xMax, yMax) %}
                {% set probe_count = "%d,%d"|format(xProbeCnt, yProbeCnt) %}
                {% set mesh_center = "%d,%d"|format(xCenter, yCenter) %}
                RESPOND MSG="Computed mesh parameters: MESH_MIN={mesh_min} MESH_MAX={mesh_max} MESH_CENTER={mesh_center} PROBE_COUNT={probe_count} RELATIVE_REFERENCE_INDEX={rRefIndex} ALGORITHM={algo}"
                SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=do_mesh VALUE={True}
                SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=do_nominal VALUE={False}
                SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=mesh_min VALUE='"{mesh_min}"'
                SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=mesh_max VALUE='"{mesh_max}"'
                SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=mesh_center VALUE='"{mesh_center}"'
                SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=probe_count VALUE='"{probe_count}"'
                SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=rri VALUE={rRefIndex}
                SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=algo VALUE='"{algo}"'
            {% else %}
                RESPOND MSG="Computed mesh parameters: none, bed mesh not needed for very small parts"
                SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=do_mesh VALUE={False}
                {% set xCenter = xMin + ((xMax - xMin) / 2) %}
                {% set yCenter = yMin + ((yMax - yMin) / 2) %}
                {% set mesh_center = "%d,%d"|format(xCenter, yCenter) %} # we still compute the mesh center for those using klipper_z_calibration
                SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=mesh_center VALUE='"{mesh_center}"'
            {% endif %}
        {% else %}
            {% set xProbeCnt = [3, xProbeCnt]|max %}
            {% set yProbeCnt = [3, yProbeCnt]|max %}

            # We verify that the number of probe points on each axis is odd or add one to it
            # This is very important to have a relative_reference_index point at the center of the mesh !
            {% if xProbeCnt % 2 == 0 %}
                {% set xProbeCnt = xProbeCnt + 1 %}
            {% endif %}
            {% if yProbeCnt % 2 == 0 %}
                {% set yProbeCnt = yProbeCnt + 1 %}
            {% endif %}

            # Check of the probe points and interpolation algorithms according to Klipper code
            {% if xMeshPPS != 0 or yMeshPPS != 0 %}
                {% set probeCntMin = [xProbeCnt, yProbeCnt]|min %}
                {% set probeCntMax = [xProbeCnt, yProbeCnt]|max %}
                {% if algo == "lagrange" and probeCntMax > 6 %}
                    # Lagrange interpolation tends to oscillate when using more than 6 samples: swith to bicubic
                    {% set algo = "bicubic" %}
                {% endif %}
                {% if algo == "bicubic" and probeCntMin < 4 %}
                    {% if probeCntMax > 6 %}
                        # Impossible case: need to add probe point on the small axis to be >= 4 (we want 5 to keep it odd)
                        {% if xProbeCnt > yProbeCnt %}
                            {% set yProbeCnt = 5 %}
                        {% else %}
                            {% set xProbeCnt = 5 %}
                        {% endif %}
                    {% else %}
                        # In this case bicubic is not adapted (less than 4 points): switch to lagrange
                        {% set algo = "lagrange" %}
                    {% endif %}
                {% endif %}
            {% endif %}

            # 5 ----- COMPUTE THE RELATIVE_REFERENCE_INDEX POINT --------------------
            {% set rRefIndex = (((xProbeCnt * yProbeCnt) - 1) / 2)|int %}
            {% set xCenter = xMin + ((xMax - xMin) / 2) %}
            {% set yCenter = yMin + ((yMax - yMin) / 2) %}

            # 6 ----- FORMAT THE PARAMETERS AND SAVE THEM ---------------------------
            {% set mesh_min = "%d,%d"|format(xMin, yMin) %}
            {% set mesh_max = "%d,%d"|format(xMax, yMax) %}
            {% set probe_count = "%d,%d"|format(xProbeCnt, yProbeCnt) %}
            {% set mesh_center = "%d,%d"|format(xCenter, yCenter) %}
            RESPOND MSG="Computed mesh parameters: MESH_MIN={mesh_min} MESH_MAX={mesh_max} MESH_CENTER={mesh_center} PROBE_COUNT={probe_count} RELATIVE_REFERENCE_INDEX={rRefIndex} ALGORITHM={algo}"
            SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=do_mesh VALUE={True}
            SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=do_nominal VALUE={False}
            SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=mesh_min VALUE='"{mesh_min}"'
            SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=mesh_max VALUE='"{mesh_max}"'
            SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=mesh_center VALUE='"{mesh_center}"'
            SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=probe_count VALUE='"{probe_count}"'
            SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=rri VALUE={rRefIndex}
            SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=algo VALUE='"{algo}"'
        {% endif %}
    {% else %}
        SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=do_mesh VALUE={True}
        SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=do_nominal VALUE={True}

        {% set xCenter = xMinConf + ((xMaxConf - xMinConf) / 2) %}
        {% set yCenter = yMinConf + ((yMaxConf - yMinConf) / 2) %}
        {% set mesh_center = "%d,%d"|format(xCenter, yCenter) %} # we still compute the mesh center for those using klipper_z_calibration
        SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=mesh_center VALUE='"{mesh_center}"'
    {% endif %}

    # Finaly save in the variables that we already computed the values
    SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=ready VALUE={True}


[gcode_macro ADAPTIVE_BED_MESH]
description: Perform a bed mesh, but only where and when it's needed
gcode:
    {% set ready = printer["gcode_macro _ADAPTIVE_MESH_VARIABLES"].ready %}

    {% if not 'xyz' in printer.toolhead.homed_axes %}
        { action_raise_error("Must Home printer first!") }
    {% endif %}

    # If the parameters where computed, we can do the mesh by calling the _DO_ADAPTIVE_MESH
    {% if ready %}
        _DO_ADAPTIVE_MESH

    # If the parameters where not computed prior to the ADAPTIVE_BED_MESH call, we call the COMPUTE_MESH_PARAMETERS
    # macro first and then call the _DO_ADAPTIVE_MESH macro after it
    {% else %}
        RESPOND MSG="Adaptive bed mesh: parameters not already computed, automatically calling the COMPUTE_MESH_PARAMETERS macro prior to the mesh"
        COMPUTE_MESH_PARAMETERS {rawparams}
        M400 # mandatory to flush the gcode buffer and be sure to use the last computed parameters
        _DO_ADAPTIVE_MESH
    {% endif %}


[gcode_macro _DO_ADAPTIVE_MESH]
gcode:
    # 1 ----- POPULATE BEDMESH PARAMS FROM SAVED VARIABLES ----------------------
    {% set do_mesh = printer["gcode_macro _ADAPTIVE_MESH_VARIABLES"].do_mesh %}
    {% set do_nominal = printer["gcode_macro _ADAPTIVE_MESH_VARIABLES"].do_nominal %}
    {% set mesh_min = printer["gcode_macro _ADAPTIVE_MESH_VARIABLES"].mesh_min %}
    {% set mesh_max = printer["gcode_macro _ADAPTIVE_MESH_VARIABLES"].mesh_max %}
    {% set probe_count = printer["gcode_macro _ADAPTIVE_MESH_VARIABLES"].probe_count %}
    {% set rRefIndex = printer["gcode_macro _ADAPTIVE_MESH_VARIABLES"].rri %}
    {% set algo = printer["gcode_macro _ADAPTIVE_MESH_VARIABLES"].algo %}

    # 2 --------- ADAPTIVE_BED_MESH LOGIC --------------------------------------

    # If it's necessary to do a mesh
    {% if do_mesh %}
        # If it's a standard bed_mesh to be done
        {% if do_nominal %}
            RESPOND MSG="Adaptive bed mesh: nominal bed mesh"
            BED_MESH_CALIBRATE
        {% else %}
            {% if printer["configfile"].config["bed_mesh"]["relative_reference_index"] is defined %}
                RESPOND MSG="Adaptive bed mesh: MESH_MIN={mesh_min} MESH_MAX={mesh_max} PROBE_COUNT={probe_count} RELATIVE_REFERENCE_INDEX={rRefIndex} ALGORITHM={algo}"
                BED_MESH_CALIBRATE MESH_MIN={mesh_min} MESH_MAX={mesh_max} PROBE_COUNT={probe_count} RELATIVE_REFERENCE_INDEX={rRefIndex} ALGORITHM={algo}
            {% else %}
                RESPOND MSG="Adaptive bed mesh: MESH_MIN={mesh_min} MESH_MAX={mesh_max} PROBE_COUNT={probe_count} ALGORITHM={algo}"
                BED_MESH_CALIBRATE MESH_MIN={mesh_min} MESH_MAX={mesh_max} PROBE_COUNT={probe_count} ALGORITHM={algo}
            {% endif %}
        {% endif %}
    {% else %}
        RESPOND MSG="Adaptive bed mesh: no mesh to be done"
    {% endif %}

    # Set back the 'ready' parameter to false
    SET_GCODE_VARIABLE MACRO=_ADAPTIVE_MESH_VARIABLES VARIABLE=ready VALUE={False}